#!python3.3
# -*- coding: utf-8 -*-
"""
System utility and information module for NoiseBot. Mostly an interface to functions in the stdlib os and platform modules and psutil.
psutil is an optional dependency. https://code.google.com/p/psutil
"""

import queue
import threading
import types
import platform
import socket
import ipaddress

try:
	import psutil
except ImportError:
	psutil_state = 'unavailable'
else:
	psutil_state = 'available'

import pynoisebot

class die_if_error:
	"""
	If an unhandled exception is raised in a thread, this decorator is useful for notifying the main thread.
	"""
	def __init__(self, name):
		self.thread_name = name

	def __call__(self, func):
		def wrapper(*args, **kwargs):
			try:
				func(*args, **kwargs)
			except BaseException as exc:
				die('The {} thread died!'.format(self.thread_name), exc)
		return wrapper

def admin_command(func):
	def wrapper(message_namespace):
		if message_namespace.sender_is_owner:
			func(message_namespace)
		else:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'Only an owner may use this command.')
			raise pynoisebot.PermissionDenied
	return wrapper

@admin_command
def uname(message_namespace):
	uname_namedtuple = platform.uname()
	uname_str = ' '.join((uname_namedtuple.system, uname_namedtuple.node, uname_namedtuple.release, uname_namedtuple.version, uname_namedtuple.machine))
	send_privmsg(message_namespace.config_network_name, message_namespace.source, uname_str)
uname.help_message = "Displays the same information as 'uname -a' would."
uname.usage_message = ''

def isup(message_namespace):
	args = message_namespace.message.split()
	if len(args) == 0:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'No address specified.')
		raise pynoisebot.InvalidArguments
	elif len(args) == 1:
		given_address = args[0]
		port = 80
		timeout = 10
	elif len(args) == 2:
		given_address, port = args
		timeout = 10
	elif len(args) == 3:
		given_address, port, timeout = args
	else:
		pass
	invalid_args = False
	try:
		port = int(port)
		if not port in range(1, 65535):
			raise ValueError
	except ValueError:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{} is not a valid port number.'.format(args[1]))
		invalid_args = True
	prefer_ipv6 = False
	try:
		gai = socket.getaddrinfo(given_address, port)
	except socket.gaierror:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Could not get an IP address for {}.'.format(given_address))
		raise pynoisebot.InvalidArguments
	if prefer_ipv6:
		sockaddr = gai[0][4]
	else:
		try:
			sockaddr = gai[1][4]
		except IndexError:
			sockaddr = gai[0][4]
	address = sockaddr[0]
	port = sockaddr[1]
	try:
		address = ipaddress.IPv4Address(address)
	except ipaddress.AddressValueError:
		try:
			address = ipaddress.IPv6Address(address)
		except ipaddress.AddressValueError:
			send_privmsg(message_namespace.config_network_name, message_namespace.source, '"{}" is not a valid IP address. Do not add protocol prefixes like http://.'.format(address))
			invalid_args = True
	try:
		timeout = float(timeout)
	except ValueError:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{} is not a valid timeout value.'.format(args[1]))
		invalid_args = True
	if invalid_args:
		raise pynoisebot.InvalidArguments
	if message_namespace.sender_is_owner:
		disallow_private = False
		disallow_link_local = False
		disallow_multicast = False
		disallow_unspecified = False
		disallow_reserved = False
		disallow_loopback = False
	else:
		disallow_private = True
		disallow_link_local = True
		disallow_multicast = True
		disallow_unspecified = True
		disallow_reserved = True
		disallow_loopback = True
	disallow_ipv6 = False
	disallowed_ipv4_addresses = ipaddress.IPv4Network('0.0.0.0/32')
	disallowed_ipv6_addresses = ipaddress.IPv6Network('::/128')
	for address_is_disallowed in [(disallow_private and address.is_private),
							(disallow_link_local and address.is_link_local),
							(disallow_multicast and address.is_multicast),
							(disallow_unspecified and address.is_unspecified),
							(disallow_reserved and address.is_reserved),
							(disallow_loopback and address.is_loopback),
							(disallow_ipv6 and address.version is 6),
							(address in disallowed_ipv4_addresses),
							(address in disallowed_ipv6_addresses)]:
		if address_is_disallowed:
			send_notice(message_namespace.config_network_name, message_namespace.nick, 'You are not allowed to check the address {}'.format(str(address)))
			raise pynoisebot.PermissionDenied
	try:
		sock = socket.create_connection((str(address), port), timeout)
	except socket.timeout:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{} appears to be down or is dropping packets from this address.'.format(given_address))
	except ConnectionRefusedError:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{0} appears to be up, but is not listening on port {1}.'.format(given_address, str(port)))
	except OSError as exc:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{}'.format(exc.args[1]))
	else:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{0} appears to be up and listening on port {1}.'.format(given_address, str(port)))
		sock.shutdown(socket.SHUT_RDWR)
		sock.close()
isup.help_message = 'Attempts to connect to the given host or IP address to see if it is up.'
isup.usage_message = ' <hostname or IP> [<port>] [<timeout in seconds>]'

def sysutils_help(message_namespace):
	"""
	Every command function should have a help_message and usage_message attribute (see examples above).
	"""
	global module_name
	trigger = connected_servers[message_namespace.config_network_name].trigger
	if not message_namespace.message:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'The sysutils module displays platform and process information. psutils functions are {}.'.format(psutil_state))
		send_privmsg(message_namespace.config_network_name, message_namespace.source, 'Type {0}commands {1} to see available commands.'.format(trigger, module_name))
		return
	try:
		command_func = commands_to_functions[message_namespace.message]
	except KeyError:
		print('Got a help request for an unregistered command... ({})'.format(message_namespace.message))
		return
	else:
		send_privmsg(message_namespace.config_network_name, message_namespace.source, command_func.help_message)
		send_privmsg(message_namespace.config_network_name, message_namespace.source, '{0}{1}{2}'.format(trigger, message_namespace.message, command_func.usage_message))

def send_privmsg(server_name, dest, message):
	outgoing_message_dicts.put({'action':'irc_message', 'server':connected_servers[server_name].config_network_name, 'message_type':'privmsg', 'args':dest, 'message':message})

def send_notice(server_name, dest, message):
	outgoing_message_dicts.put({'action':'irc_message', 'server':connected_servers[server_name].config_network_name, 'message_type':'notice', 'args':dest, 'message':message})

json_encoder = pynoisebot.json_encoder
json_decoder = pynoisebot.json_decoder

@die_if_error(name='Receive Messages From Bot')
def recv_messages_from_bot(connection):
	while alive:
		incoming_json = connection.recv()
		incoming_messages.put(json_decoder.decode(incoming_json))

@die_if_error(name='Send Messages To Bot')
def send_messages_to_bot(connection):
	while alive:
		outgoing_message_dict = outgoing_message_dicts.get()
		outgoing_message_json = json_encoder.encode(outgoing_message_dict)
		pynoisebot.send_message(connection, outgoing_message_json)
		outgoing_message_dicts.task_done()

@die_if_error(name='Incoming Messages')
def process_incoming_messages():
	"""
	print() should be replaced with calls to a logger you create.
	"""
	print('Started incoming message process thread')
	while alive:
		incoming_message = types.SimpleNamespace(**incoming_messages.get())
		print(incoming_message)
		if incoming_message.action == 'irc_message':
			try:
				command_func = commands_to_functions[incoming_message.command]
			except KeyError:
				print('Got a command that isn\'t in commands_to_functions: {}'.format(incoming_message.command))
			else:
				print('Got a command: {}'.format(incoming_message.command))
				threading.Thread(target=process_command, args=(command_func, incoming_message), daemon=False).start()
		elif incoming_message.action in ('error', 'die'):
			print('Error message from bot: {}'.format(incoming_message.reason))
			die()
		elif incoming_message.action == 'ping':
			pynoisebot.ping_bot(connection)
		elif incoming_message.action == 'server_info':
			if incoming_message.config_network_name in connected_servers.keys():
				connected_servers[incoming_message.config_network_name].update(incoming_message)
			else:
				connected_servers[incoming_message.config_network_name] = pynoisebot.ConnectedServer(incoming_message)
		elif incoming_message.action == 'channel_info':
			pass
		elif incoming_message.action == 'module_info':
			global module_name
			module_name = incoming_message.module_name
			print('Module name is {}'.format(module_name))
		else:
			print('Ignored invalid message from bot: {}'.format(incoming_message))
		incoming_messages.task_done()

psutil_commands = {}
non_psutil_commands = {'uname':uname,
						'isup':isup,
						'help':sysutils_help}
commands_to_functions = {}
commands_to_functions.update(non_psutil_commands)
commands_to_functions.update(psutil_commands)

def process_command(func, message_namespace):
	try:
		func(message_namespace)
	except (pynoisebot.PermissionDenied, pynoisebot.InvalidArguments):
		pass # the errors are already handled - these exceptions abort the call by propagating up to this point

def die(reason, exc):
	global alive
	global connection
	pynoisebot.send_die_message(connection, reason)
	alive = False
	connection.close()
	raise exc

alive = True
connection = pynoisebot.establish_connection(('localhost', 20153))
recv_messages_from_bot_thread = threading.Thread(target=recv_messages_from_bot, args=(connection,), daemon=True)
send_messages_to_bot_thread = threading.Thread(target=send_messages_to_bot, args=(connection,), daemon=True)
incoming_messages = queue.Queue()
outgoing_message_dicts = queue.Queue()
commands_to_register = frozenset(commands_to_functions.keys()).difference({'help'})
pynoisebot.register_module(conn=connection, recv_events=False, recv_commands=True, recv_message_types=(), commands=tuple(commands_to_register))
print('registered module')
recv_messages_from_bot_thread.start()
send_messages_to_bot_thread.start()
connected_servers = {}
process_incoming_messages()